using System;
using System.Collections;
using System.Collections.Generic;

namespace LightCAD.Three
{
    public class Plane
    {
        #region scope properties or methods
        //private static Vector3 _vector1 = new Vector3();
        //private static Vector3 _vector2 = new Vector3();
        //private static Matrix3 _normalMatrix = new Matrix3();
        private PlaneContext getContext()
        {
            return ThreadContext.getCurrThreadContext().PlaneCtx;
        }
        #endregion

        #region Properties

        public Vector3 normal;
        public double constant;
        internal DateTime lastChangedTime;
        public string name;
        #endregion

        #region constructor
        public Plane(Vector3 normal = null, double constant = 0)
        {
            if (normal == null) normal = new Vector3(1, 0, 0);
            // normal is assumed to be normalized
            this.normal = normal;
            this.constant = constant;
        }
        #endregion

        #region methods
        public Plane set(Vector3 normal, double constant)
        {
            this.normal.copy(normal);
            this.constant = constant;
            return this;
        }
        public Plane setComponents(double x, double y, double z, double w)
        {
            this.normal.set(x, y, z);
            this.constant = w;
            return this;
        }
        public Plane setFromNormalAndCoplanarPoint(Vector3 normal, Vector3 point)
        {
            this.normal.copy(normal);
            this.constant = -point.dot(this.normal);
            return this;
        }
        public Plane setFromCoplanarPoints(Vector3 a, Vector3 b, Vector3 c)
        {
            var ctx = getContext();
            var _vector1 = ctx._vector1;
            var _vector2 = ctx._vector2;
            Vector3 normal = _vector1.subVectors(c, b).cross(_vector2.subVectors(a, b)).normalize();
            // Q: should an error be thrown if normal is zero (e.g. degenerate plane)?
            this.setFromNormalAndCoplanarPoint(normal, a);
            return this;
        }
        public Plane copy(Plane plane)
        {
            this.normal.copy(plane.normal);
            this.constant = plane.constant;
            return this;
        }
        public Plane normalize()
        {
            // Note: will lead to a divide by zero if the plane is invalid.
            double inverseNormalLength = 1.0 / this.normal.length();
            this.normal.multiplyScalar(inverseNormalLength);
            this.constant *= inverseNormalLength;
            return this;
        }
        public Plane negate()
        {
            this.constant *= -1;
            this.normal.negate();
            return this;
        }
        public double distanceToPoint(Vector3 point)
        {
            return this.normal.dot(point) + this.constant;
        }
        public double distanceToSphere(Sphere sphere)
        {
            return this.distanceToPoint(sphere.center) - sphere.radius;
        }
        public Vector3 projectPoint(Vector3 point, Vector3 target = null)
        {
            target = target ?? new Vector3();
            return target.copy(point).addScaledVector(this.normal, -this.distanceToPoint(point));

            //return target.copy(this.normal).multiplyScalar(-this.distanceToPoint(point)).add(point);
        }
        public Vector3 intersectLine(Line3 line, Vector3 target = null)
        {
            target = target ?? new Vector3();
            var _vector1 = getContext()._vector1;
            Vector3 direction = line.delta(_vector1);
            double denominator = this.normal.dot(direction);
            if (denominator == 0)
            {
                // line is coplanar, return origin
                if (this.distanceToPoint(line.start) == 0)
                {
                    return target.copy(line.start);
                }
                // Unsure if this is the correct method to handle this case.
                return null;
            }
            double t = -(line.start.dot(this.normal) + this.constant) / denominator;
            if (t < 0 || t > 1)
            {
                return null;
            }
            return target.copy(line.start).addScaledVector(direction, t);
            //return target.copy(direction).multiplyScalar(t).add(line.start);
        }
        public bool intersectsLine(Line3 line)
        {
            // Note: this tests if a line intersects the plane, not whether it (or its end-points) are coplanar with it.
            double startSign = this.distanceToPoint(line.start);
            double endSign = this.distanceToPoint(line.end);
            return (startSign < 0 && endSign > 0) || (endSign < 0 && startSign > 0);
        }
        public bool intersectsBox(Box3 box)
        {
            return box.intersectsPlane(this);
        }
        public bool intersectsSphere(Sphere sphere)
        {
            return sphere.intersectsPlane(this);
        }
        public Vector3 coplanarPoint(Vector3 target)
        {
            return target.copy(this.normal).multiplyScalar(-this.constant);
        }
        public Plane applyMatrix4(Matrix4 matrix, Matrix3 optionalNormalMatrix = null)
        {
            var ctx = getContext();
            var _normalMatrix = ctx._normalMatrix;
            var _vector1 = ctx._vector1;
            Matrix3 normalMatrix = optionalNormalMatrix ?? _normalMatrix.getNormalMatrix(matrix);
            Vector3 referencePoint = this.coplanarPoint(_vector1).applyMatrix4(matrix);
            Vector3 normal = this.normal.applyMatrix3(normalMatrix).normalize();
            this.constant = -referencePoint.dot(normal);
            return this;
        }
        public Plane translate(Vector3 offset)
        {
            this.constant -= offset.dot(this.normal);
            return this;
        }
        public bool equals(Plane plane)
        {
            return plane.normal.equals(this.normal) && (plane.constant == this.constant);
        }
        public Plane clone()
        {
            return new Plane().copy(this);
        }
        #endregion

    }
}
